package jp.dodododo.dao.object;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import jp.dodododo.dao.exception.PropertyNotFoundRuntimeException;
import jp.dodododo.dao.message.Message;
import jp.dodododo.dao.util.CacheUtil;
import jp.dodododo.dao.util.CaseInsensitiveMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Beanではなくて、プロパティー名をDB寄りとして扱う。<br>
 * 大文字、小文字を無視。キャメルケース、スネークケースとかも無視。<br>
 * なので、BeanDescではなくて、ObjectDesc。
 * 
 * @author Satoshi Kimura
 */
public class ObjectDesc {

	private static final Log logger = LogFactory.getLog(ObjectDesc.class);

	private Class<?> targetClass;

	private Map<String, PropertyDesc> propertyDescCache = CacheUtil.cacheMap(new CaseInsensitiveMap<PropertyDesc>());

	private List<PropertyDesc> propertyDescList = CacheUtil.cacheList();

	public ObjectDesc(Class<?> targetClass) {
		if (targetClass == null) {
			throw new NullPointerException("targetClass");
		}
		this.targetClass = targetClass;
		setupPropertyDescs();
	}

	public Class<?> getTargetClass() {
		return targetClass;
	}

	//
	public boolean hasPropertyDesc(String propertyName) {
		return this.propertyDescCache.containsKey(propertyName);
	}

	public PropertyDesc getPropertyDesc(String propertyName) {
		PropertyDesc pd = getPropertyDescNoException(propertyName);
		if (pd == null) {
			throw new PropertyNotFoundRuntimeException(targetClass, propertyName);
		}
		return pd;
	}

	public PropertyDesc getPropertyDescNoException(String propertyName) {
		return propertyDescCache.get(propertyName);
	}

	private void setupPropertyDescs() {
		Map<String, PropertyDesc> propertyDescTmpCache = new CaseInsensitiveMap<PropertyDesc>();
		List<PropertyDesc> propertyDescTmpList = new ArrayList<PropertyDesc>();

		Field[] fields = targetClass.getDeclaredFields();
		for (Field f : fields) {
			setupPropertyDescs(f, propertyDescTmpCache, propertyDescTmpList);
		}
		Method[] methods = targetClass.getMethods();
		for (Method m : methods) {
			setupPropertyDescs(m, propertyDescTmpCache, propertyDescTmpList);
		}
		for (Iterator<PropertyDesc> iter = propertyDescTmpList.iterator(); iter.hasNext();) {
			PropertyDesc pd = iter.next();
			if (pd.isReadable() == true || pd.isWritable()) {
				propertyDescCache.put(pd.getPropertyName(), pd);
				propertyDescList.add(pd);
			}
		}
	}

	private void setupPropertyDescs(Field f, Map<String, PropertyDesc> propertyDescTmpCache, List<PropertyDesc> propertyDescTmpList) {
		if (f.isSynthetic()) {
			return;
		}
		String propertyName = f.getName();
		PropertyDesc propDesc = new PropertyDesc(propertyName, f, this);
		addPropertyDesc(propertyName, propDesc, propertyDescTmpCache, propertyDescTmpList);
	}

	private void setupPropertyDescs(Method m, Map<String, PropertyDesc> propertyDescTmpCache, List<PropertyDesc> propertyDescTmpList) {
		if (m.isBridge() || m.isSynthetic() || m.isVarArgs()) {
			return;
		}
		String methodName = m.getName();
		Class<?> returnType = m.getReturnType();
		if (methodName.startsWith("get")) {
			if (m.getParameterTypes().length == 0 && returnType.equals(void.class) == false && methodName.equals("getClass") == false) {
				String propertyName = methodName.substring(3);
				addReadMethod(m, propertyName, returnType, propertyDescTmpCache, propertyDescTmpList);
			}
		} else if (methodName.startsWith("is")) {
			if (m.getParameterTypes().length == 0 && (returnType.equals(Boolean.TYPE) || returnType.equals(Boolean.class))) {
				String propertyName = methodName.substring(2);
				addReadMethod(m, propertyName, returnType, propertyDescTmpCache, propertyDescTmpList);
			}
		} else if (methodName.startsWith("set")) {
			if (m.getParameterTypes().length == 1 && returnType.equals(void.class)) {
				String propertyName = methodName.substring(3);
				addWriteMethod(m, propertyName, m.getParameterTypes()[0], propertyDescTmpCache, propertyDescTmpList);
			}
		}
	}

	private void addPropertyDesc(String propertyName, PropertyDesc propertyDesc, Map<String, PropertyDesc> propertyDescTmpCache,
			List<PropertyDesc> propertyDescTmpList) {
		if (propertyName == null) {
			throw new NullPointerException("propertyName");
		}
		if (propertyDesc == null) {
			throw new NullPointerException("propertyDesc");
		}

		propertyDescTmpCache.put(propertyName, propertyDesc);
		propertyDescTmpList.add(propertyDesc);
	}

	private <PROPERTY> void addReadMethod(Method readMethod, String propertyName, Class<PROPERTY> propertyType,
			Map<String, PropertyDesc> propertyDescTmpCache, List<PropertyDesc> propertyDescTmpList) {
		PropertyDesc propDesc = propertyDescTmpCache.get(propertyName);
		if (propDesc != null) {
			if (propDesc.getPropertyType().equals(propertyType) == false) {
				if (propDesc.getWriteMethod() != null) {
					logger.warn(Message.getMessage("00007", propDesc.getWriteMethod().getName(), propDesc.getPropertyType().getName(),
							readMethod.getName(), propertyType.getName()));
				} else {
					propDesc.setReadMethod(readMethod);
					propDesc.setPropertyType(propertyType);
				}
			} else {
				propDesc.setReadMethod(readMethod);
			}
		} else {
			propDesc = new PropertyDesc(propertyName, readMethod, null, this);
			addPropertyDesc(propertyName, propDesc, propertyDescTmpCache, propertyDescTmpList);
		}
	}

	private <PROPERTY> void addWriteMethod(Method writeMethod, String propertyName, Class<PROPERTY> propertyType,
			Map<String, PropertyDesc> propertyDescTmpCache, List<PropertyDesc> propertyDescTmpList) {
		PropertyDesc propDesc = propertyDescTmpCache.get(propertyName);
		if (propDesc != null) {
			if (propDesc.getPropertyType().equals(propertyType) == false) {
				if (propDesc.getReadMethod() != null) {
					logger.warn(Message.getMessage("00007", propDesc.getReadMethod().getName(), propDesc.getPropertyType().getName(),
							writeMethod.getName(), propertyType.getName()));
				} else {
					propDesc.setWriteMethod(writeMethod);
					propDesc.setPropertyType(propertyType);
				}
			} else {
				propDesc.setWriteMethod(writeMethod);
			}
		} else {
			propDesc = new PropertyDesc(propertyName, null, writeMethod, this);
			addPropertyDesc(propertyName, propDesc, propertyDescTmpCache, propertyDescTmpList);
		}
	}

	public int getPropertyDescSize() {
		return propertyDescList.size();
	}

	public PropertyDesc getPropertyDesc(int index) {
		return propertyDescList.get(index);
	}

}
